import { Point, IControlRect, IControlPoint, ControlTypeEnum, CursorEnum } from '../model';
import { PointUtil, Vector2Util, Vector2 } from '../utils';


export class BoundingBox {

    /**
     * control bounding box
     */
    controlRect!: IControlRect;
    shapeCenterPoint: Point = { x: 0, y: 0 };
    borderCenterPoint: Point = { x: 0, y: 0 };
    controlPoint!: IControlPoint;
    strokeStyle = '#7a5fe3';
    lineWidth = 2;

    scaleX = 1;
    scaleY = 1;
    translateX = 0;
    translateY = 0;
    rotate = 0;


    controlType: ControlTypeEnum | boolean = false;

    isDisbaleSelected = false;

    isSelected = false;

    isHasTopControl = true;

    // lastMousePoint and lastState record move position
    private lastMousePoint: Point = { x: 0, y: 0 };
    private lastState = {
        translateX: 0,
        translateY: 0,
        scaleX: 0,
        scaleY: 0,
        rotate: 0
    };

    tempCtx!: CanvasRenderingContext2D;

    /**
     * control block width
     */
    readonly controlBlock = 8;



    constructor() {

    }

    /**
     * update current shape point
     * @param point current mouse point
     * @param mouseDownPoint mouse down point
     */
    update(point: Point, mouseDownPoint: Point) {
        const { x, y, width, height } = this.controlRect;
        if (this.lastMousePoint !== mouseDownPoint) {
            this.lastMousePoint = mouseDownPoint;
            this.lastState = {
                translateX: this.translateX,
                translateY: this.translateY,
                scaleX: this.scaleX,
                scaleY: this.scaleY,
                rotate: this.rotate
            };
        }
        let translateX = this.translateX;
        let translateY = this.translateY;
        const px = point.x;
        const py = point.y;
        const controlType = this.controlType;
        switch (controlType) {
            case ControlTypeEnum.TL:
                {
                    translateX = px - x;
                    translateY = py - y;
                    this.scaleY = (y + this.lastState.translateY - py) / height + this.lastState.scaleY;
                    this.scaleX = (x + this.lastState.translateX - px) / width + this.lastState.scaleX;
                }
                break;
            case ControlTypeEnum.ML:
                {
                    translateX = px - x;
                    this.scaleX = (x + this.lastState.translateX - px) / width + this.lastState.scaleX;
                }
                break;
            case ControlTypeEnum.BL:
                {
                    translateX = px - x;
                    this.scaleY = (py - y - this.translateY) / height;
                    this.scaleX = (x + this.lastState.translateX - px) / width + this.lastState.scaleX;
                }
                break;
            case ControlTypeEnum.TC:
                {
                    translateY = py - y;
                    this.scaleY = (y + this.lastState.translateY - py) / height + this.lastState.scaleY;
                }
                break;
            case ControlTypeEnum.BC:
                {
                    this.scaleY = (py - y - this.translateY) / height;
                }
                break;
            case ControlTypeEnum.TR:
                {
                    translateY = py - y;
                    this.scaleY = (y + this.lastState.translateY - py) / height + this.lastState.scaleY;
                    this.scaleX = (px - x - this.translateX) / width;
                }
                break;
            case ControlTypeEnum.MR:
                {
                    this.scaleX = (px - x - this.translateX) / width;
                }
                break;
            case ControlTypeEnum.BR:
                {
                    this.scaleX = (px - x - this.translateX) / width;
                    this.scaleY = (py - y - this.translateY) / height;
                }
                break;
            case ControlTypeEnum.ROTATE:
                {
                    this.rotate = this.getRotateRadian(point);
                }
                break;
            default:
                // only move, not on border
                {
                    translateX = px - this.lastMousePoint.x + this.lastState.translateX;
                    translateY = py - this.lastMousePoint.y + this.lastState.translateY;
                }
                break;
        }
        this.translateX = translateX;
        this.translateY = translateY;
    }

    getShapeCenterPoint() {
        const { x, y, width, height } = this.controlRect;
        this.shapeCenterPoint = {
            x: x + width / 2,
            y: y + height / 2
        };
    }

    protected getRotateRadian(currentPoint: Point) {
        const { tc, ml, rotate } = this.controlPoint;
        const cPoint = { x: tc.x, y: ml.y };
        this.borderCenterPoint = cPoint;
        // the vector range is [0, PI], so the transform range is [-PI, PI]
        const result = Vector2Util.angleVector(new Vector2(rotate.x - cPoint.x, rotate.y - cPoint.y),
            new Vector2(currentPoint.x - cPoint.x, currentPoint.y - cPoint.y));
        return currentPoint.x - cPoint.x < 0 ? -result : result;
    }

    /**
     * mouse origin point
     * @param point current mouse point
     * @returns origin point
     */
    protected transferOriginPoint(point: Point) {

        const { x, y } = this.controlRect;
        let px = point.x - this.translateX;
        let py = point.y - this.translateY;

        px = (px - x) / this.scaleX + x;
        py = (py - y) / this.scaleY + y;

        const changeResult = this.getRatatePoint({ x: px, y: py });
        // delete those data
        this.tempCtx.beginPath();
        this.tempCtx.arc(changeResult.x, changeResult.y, 2, 0, Math.PI * 2);
        this.tempCtx.stroke();
        return changeResult;
    }

    getRatatePoint(point: Point, centerPoint?: Point) {
        const { x, y } = centerPoint || this.shapeCenterPoint;
        const vChange = new Vector2(point.x - x, point.y - y);
        const r = vChange.length();
        const rotateRadian = Math.atan2(point.y - y, point.x - x) - this.rotate;
        return {
            x: x + r * Math.cos(rotateRadian),
            y: y + r * Math.sin(rotateRadian)
        };
    }

    /**
     * handle object position
     * @param ctx canvas context
     */
    handleAction(ctx: CanvasRenderingContext2D) {
        this.tempCtx = ctx;
        const { x, y, width, height } = this.controlRect;
        ctx.beginPath();

        // **warnging: need to rotate first **
        ctx.translate(this.translateX + x + width / 2, this.translateY + y + height / 2);
        ctx.rotate(this.rotate);
        ctx.translate(-this.translateX - x - width / 2, -this.translateY - y - height / 2);

        ctx.translate(-x * this.scaleX, -y * this.scaleY);
        ctx.scale(this.scaleX, this.scaleY);
        ctx.translate((this.translateX + x) / this.scaleX, (y + this.translateY) / this.scaleY);
    }

    /**
     * rotate shape
     * @param ctx canvas context
     */
    protected handleRotate(ctx: CanvasRenderingContext2D, point?: Point) {
        const { x, y } = point || this.shapeCenterPoint;
        ctx.translate(x, y);
        ctx.rotate(this.rotate);
        ctx.translate(-x, -y);
    }

    /**
     * draw control border
     * @param ctx canvas context
     */
    drawBorder(ctx: CanvasRenderingContext2D) {
        const { x, y, width, height } = this.controlRect;
        ctx.save();
        this.getControlPoints();
        ctx.beginPath();
        this.handleRotate(ctx, this.borderCenterPoint);
        ctx.lineWidth = this.lineWidth;
        ctx.strokeStyle = this.strokeStyle;
        // draw border
        ctx.rect(x + this.translateX, y + this.translateY, this.scaleX * width, this.scaleY * height);
        if (this.isHasTopControl) {
            // draw top control line
            ctx.moveTo(x + this.translateX + width * this.scaleX / 2, y + this.translateY);
            ctx.lineTo(x + this.translateX + width * this.scaleX / 2, y + this.translateY - this.controlBlock * 2);
        }
        ctx.stroke();
        ctx.restore();
    }

    /**
     * draw control block
     * @param ctx canvas context
     */
    drawControls(ctx: CanvasRenderingContext2D) {
        const w = this.controlBlock;
        const padding = w / 2;
        ctx.save();
        this.getControlPoints();
        ctx.beginPath();
        this.handleRotate(ctx, this.borderCenterPoint);
        ctx.fillStyle = this.strokeStyle;
        const { rotate, ...controlPoint } = this.controlPoint;
        Object.values(controlPoint).forEach((controlItem: Point) => {
            ctx.rect(controlItem.x - padding, controlItem.y - padding, w, w);
        });
        if (this.isHasTopControl) {
            ctx.arc(rotate.x, rotate.y, padding, 0, Math.PI * 2);
        }
        ctx.fill();
        ctx.restore();
    }



    /**
     * get control points
     * @returns control points
     */
    getControlPoints(): IControlPoint {
        const { x, y, width, height } = this.controlRect;
        const transleteX = x + this.translateX;
        const transleteY = y + this.translateY;
        const scaleWidth = width * this.scaleX;
        const scaleHeight = height * this.scaleY;
        const xHarf = transleteX + scaleWidth / 2;
        const xTotal = transleteX + scaleWidth;
        const yHarf = transleteY + scaleHeight / 2;
        const yTotal = transleteY + scaleHeight;
        this.controlPoint = {
            tl: { x: transleteX, y: transleteY },
            tc: { x: xHarf, y: transleteY },
            tr: { x: xTotal, y: transleteY },
            ml: { x: transleteX, y: yHarf },
            mr: { x: xTotal, y: yHarf },
            bl: { x: transleteX, y: yTotal },
            bc: { x: xHarf, y: yTotal },
            br: { x: xTotal, y: yTotal },
            rotate: { x: xHarf, y: transleteY - this.controlBlock * 2 },
        };

        this.borderCenterPoint = { x: xHarf, y: yHarf };
        return this.controlPoint;
    }

    getIsInControlArea(point: Point): boolean {
        const cp = this.controlPoint;
        return PointUtil.getPointIsInsideArea(point, [cp.tl, cp.tc, cp.tr, cp.mr, cp.br, cp.bc, cp.bl, cp.ml, cp.tl]);
    }

    /**
     * get current control points
     * @param point current mouse point
     * @returns control points
     */
    getOnControlType(point: Point): { cursor: CursorEnum, controlType: ControlTypeEnum | boolean } {
        point = this.getRatatePoint(point, this.borderCenterPoint);
        const { rotate, ...cp } = this.controlPoint;
        const r = this.controlBlock / 2;

        let controlKey: ControlTypeEnum | boolean = false;
        const entryPoint = Object.entries(cp);

        for (let i = 0, length = entryPoint.length; i < length; i++) {
            const [key, pointr] = entryPoint[i];
            if (PointUtil.getCircleRadius(point, pointr) < r) {
                controlKey = key as ControlTypeEnum;
                break;
            }
        }
        if (this.isHasTopControl && PointUtil.getCircleRadius(point, rotate) < r) {
            controlKey = ControlTypeEnum.ROTATE;
        }
        let result: CursorEnum = CursorEnum.Auto;
        switch (controlKey) {
            case ControlTypeEnum.TL:
            case ControlTypeEnum.BR:
                result = CursorEnum.LeftUp;
                break;
            case ControlTypeEnum.TC:
            case ControlTypeEnum.BC:
                result = CursorEnum.UpDown;
                break;
            case ControlTypeEnum.ML:
            case ControlTypeEnum.MR:
                result = CursorEnum.LeftRight;
                break;
            case ControlTypeEnum.TR:
            case ControlTypeEnum.BL:
                result = CursorEnum.LeftDown;
                break;
            case ControlTypeEnum.ROTATE:
                result = CursorEnum.grab;
                break;
            default:
                break;
        }
        this.controlType = controlKey;
        return { cursor: result, controlType: controlKey };
    }

}
